/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2019.
 * Description: what to deal with after checking failed in CFI permissive mode.
 * Author: tengfei
 * Create: 2019-06-22
 */

#include <linux/kernel.h>
#include <linux/bug.h>
#include <linux/printk.h>
#include <linux/kprobes.h>
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/rtos_sysctl.h>
#ifdef CONFIG_RTOS_INTRUSION_DETECT
#include <linux/preempt.h>
#include <linux/audit.h>
#endif
#include <linux/hkids/hkids.h>
#include <linux/rtos_boot_optimization.h>

#define CFI_MSG_SIZE  (2 * KSYM_SYMBOL_LEN)
#define CFI_LOG_MAX 100
#define CFI_HASH_SALT (5381)

enum {
	/*
	 * For the "AUTO" level,
	 * now it is treated the same as the "LOGGING" level.
	 * later there will be a policy to decide what to deal with after
	 * checking CFI label failed.
	 */
	CFI_PERMISSIVE_AUTO = 0,

	/* the following levels will be set by manual */
	CFI_PERMISSIVE_SILENT,
	CFI_PERMISSIVE_LOGGING,
	CFI_PERMISSIVE_ABORT,

	CFI_PERMISSIVE_INVALID
};

#ifdef CONFIG_KALLSYMS
struct cfi_sym_info {
	unsigned long ofs;
	unsigned long symsize;
	char symname[KSYM_NAME_LEN];
	char modname[KSYM_NAME_LEN];
};
#endif

int sysctl_cfi_permissive_level __read_mostly = CFI_PERMISSIVE_AUTO;
int cfi_permissive_level_max __read_mostly = CFI_PERMISSIVE_ABORT;

#ifdef CONFIG_RTOS_CFI_MODE_PERMISSIVE
static inline void rtos_cfi_print(const char *msg)
{
	pr_warn("%s\n", msg);
}

struct hcfi_log_info {
	const char *file;
	unsigned int line;
	const char *func;
	void *callee;
	void *caller;
};

struct pac_log_info {
	const char *name;
	unsigned int pid;
	unsigned long pc;
	unsigned long lr;
};

struct bti_log_info {
	const char *name;
	unsigned int pid;
	unsigned int tgid;
};

static unsigned int cfi_hash_log(const char *func, unsigned int loc)
{
	unsigned int hash = CFI_HASH_SALT;
	unsigned char c;

	for (c = (unsigned char)*func; c != '\0'; c = (unsigned char)*++func)
		hash = ((hash << 5) + hash) + c;

	return ((hash & 0xffff) | (loc << 16));
}

static bool find_cfi_warn(unsigned int *hash_table, int counter, unsigned int hash_log)
{
	int cursor;

	if (counter >= CFI_LOG_MAX / 2)
		return true;

	if (counter == 0)
		return false;

	cursor = counter;
	while (cursor > 0) {
		if (hash_table[cursor - 1] == hash_log)
			return true;

		cursor--;
	}

	return false;
}

static inline void update_cfi_hash_table(unsigned int *hash_table, int *counter,
		unsigned int hash_log)
{
	hash_table[*counter] = hash_log;
	*counter = *counter + 1;
}
#endif

#ifdef CONFIG_RTOS_HCFI
static unsigned int hcfi_hash_table[CFI_LOG_MAX] = {0};
static int hcfi_counter;

#ifdef CONFIG_KALLSYMS
static int rtos_cfi_get_sym_info(const void *func, struct cfi_sym_info *psym)
{
	int res;
	uintptr_t addr = (uintptr_t) func;

	psym->modname[0] = '\0';
	res = lookup_symbol_attrs(addr, &psym->symsize, &psym->ofs,
				psym->modname, psym->symname);
	if (res == 0 && psym->modname[0] == '\0')
		strcpy(psym->modname, "kernel");
	return res;
}
#endif

#ifdef CONFIG_RTOS_INTRUSION_DETECT
static void notrace rtos_cfi_audit(const char *msg)
{
	struct audit_buffer *ab = NULL;

	if (!audit_enabled) {
		rtos_cfi_print(msg);
		return;
	}

	ab = audit_log_start(current->audit_context, GFP_ATOMIC,
				AUDIT_KERNEL_OTHER);
	if (!ab)
		return;

	audit_log_format(ab, "%s", msg);
	audit_log_end(ab);
}
#endif

static void notrace rtos_hcfi_set_msg(char *msg, int mlen, const struct hcfi_log_info *info,
				      const struct cfi_sym_info *caller)
{
	int len;
#ifdef CONFIG_KALLSYMS
	struct cfi_sym_info hcfi_sym_info;

	(void)rtos_cfi_get_sym_info(info->callee, &hcfi_sym_info);
#endif
	if (info->func && info->file)
		len = snprintf(msg, mlen,
				"HCFI FAILED: type check failed is detected at function %s in %s:%u, the target:\n",
				info->func, info->file, info->line);
	else if (caller->symname[0] != '\0')
		len = snprintf(msg, mlen,
				"HCFI FAILED: type check failed is detected at function %s+%#lx/%#lx [%s], the target:\n",
				caller->symname, caller->ofs, caller->symsize, caller->modname);
	else
		len = snprintf(msg, mlen,
				"HCFI FAILED: type check failed is detected at function <%pK>, the target:\n",
				info->caller);
	if (len >= mlen)
		return;
#ifdef CONFIG_KALLSYMS
	snprintf(msg + len, mlen - len, "\"%s+%#lx/%#lx [%s]\"",
			hcfi_sym_info.symname, hcfi_sym_info.ofs, hcfi_sym_info.symsize, hcfi_sym_info.modname);
#else
	snprintf(msg + len, mlen - len, "\"<%pK>\"", info->callee);
#endif
}

static void notrace rtos_hcfi_write_msg(const struct hcfi_log_info *info,
					const struct cfi_sym_info *caller)
{
	char hcfi_msg[CFI_MSG_SIZE];

	rtos_hcfi_set_msg(hcfi_msg, CFI_MSG_SIZE, info, caller);
#ifdef CONFIG_RTOS_INTRUSION_DETECT
	rtos_cfi_audit(hcfi_msg);
#endif
#ifdef CONFIG_HKIDS_CHK_CFI_EXCEPTION
	if (is_hkids_chk_cfi_exception_enabled() && RTOS_SEC_HKIDS_ENABLED)
		hkids_report("func=%pS msg=chk_cfi_exception key=hkids\n", info->callee);
#endif
	rtos_cfi_print(hcfi_msg);
}


#ifdef CONFIG_RTOS_HCFI_FN_ONEARG
void notrace rtos_hcfi_logger(void *callee)
#else
void notrace rtos_hcfi_logger(const char *file, unsigned int line,
			      const char *func, void *callee)
#endif
{
	int level = sysctl_cfi_permissive_level;
	unsigned int hash_log;
	struct cfi_sym_info caller_info = { 0 };
	void *caller = __builtin_return_address(0);
#ifdef CONFIG_RTOS_HCFI_FN_ONEARG
	struct hcfi_log_info info = {NULL, 0U, NULL, callee, caller};
#else
	struct hcfi_log_info info = {file, line, func, callee, caller};
#endif

	if (level == CFI_PERMISSIVE_SILENT)
		return;

#if !defined(CONFIG_RTOS_HCFI_FN_ONEARG) || defined(CONFIG_KALLSYMS)
#if defined(CONFIG_RTOS_HCFI_FN_ONEARG)
	(void)rtos_cfi_get_sym_info(caller, &caller_info);
	hash_log = cfi_hash_log(caller_info.symname, (unsigned int)(caller_info.ofs & 0xffff));
#else
	hash_log = cfi_hash_log(func, line);
#endif
	if (find_cfi_warn(hcfi_hash_table, hcfi_counter, hash_log) && level != CFI_PERMISSIVE_ABORT)
		return;
	update_cfi_hash_table(hcfi_hash_table, &hcfi_counter, hash_log);
#endif

	rtos_hcfi_write_msg(&info, &caller_info);

	switch (level) {
	case CFI_PERMISSIVE_AUTO:
	case CFI_PERMISSIVE_LOGGING:
		return;

	case CFI_PERMISSIVE_ABORT:
		BUG();
	default:
		break;
	}
}
EXPORT_SYMBOL(rtos_hcfi_logger);
NOKPROBE_SYMBOL(rtos_hcfi_logger);

static struct ctl_table rtos_cfi_sysctls[] = {
	{
		.procname       = "hcfi_permissive_level",
		.data           = &sysctl_cfi_permissive_level,
		.maxlen         = sizeof(sysctl_cfi_permissive_level),
		.mode           = 0600,
		.proc_handler   = proc_dointvec_minmax,
		.extra1         = SYSCTL_ZERO,
		.extra2         = &cfi_permissive_level_max,
	},
	{}
};

rtos_kernel_sysctls_proc_init(rtos_cfi, rtos_cfi_sysctls);
#endif

#ifdef CONFIG_RTOS_PAC_CFI_PERMISSIVE
static unsigned int pac_hash_table[CFI_LOG_MAX] = {0};
static int pac_counter;

static void notrace rtos_pac_set_msg(char *msg, int mlen, const struct pac_log_info *info)
{
	if (info->name)
		snprintf(msg, mlen,
			"PAC FAILED: at function %pS, caller %pS.",
			(void *)(info->pc), (void *)(info->lr));
}

static void notrace rtos_pac_write_msg(const struct pac_log_info *info)
{
	char pac_msg[CFI_MSG_SIZE];

	rtos_pac_set_msg(pac_msg, CFI_MSG_SIZE, info);
	rtos_cfi_print(pac_msg);
}

bool notrace rtos_pac_logger(const char *name, unsigned int pid_num, unsigned long pc,
		unsigned long lr)
{
	unsigned int hash_log;
	struct pac_log_info info = {name, pid_num, pc, lr};

	hash_log = cfi_hash_log(name, pc & 0xffffffff);
	if (find_cfi_warn(pac_hash_table, pac_counter, hash_log))
		return false;

	update_cfi_hash_table(pac_hash_table, &pac_counter, hash_log);
	rtos_pac_write_msg(&info);
	return true;
}
EXPORT_SYMBOL(rtos_pac_logger);
NOKPROBE_SYMBOL(rtos_pac_logger);
#endif

#ifdef CONFIG_RTOS_BTI_USER_PERMISSIVE
static unsigned int bti_hash_table[CFI_LOG_MAX] = {0};
static int bti_counter;

static void notrace rtos_bti_set_msg(char *msg, int mlen, const struct bti_log_info *info)
{
	if (info->name)
		snprintf(msg, mlen,
			"BTI FAILED: pid = %d, tgid = %d, comm = %s.",
			info->pid, info->tgid, info->name);
}

static void notrace rtos_bti_write_msg(const struct bti_log_info *info)
{
	char bti_msg[CFI_MSG_SIZE];

	rtos_bti_set_msg(bti_msg, CFI_MSG_SIZE, info);
	rtos_cfi_print(bti_msg);
}

void notrace rtos_bti_logger(const char *name, unsigned int pid_num, unsigned int tgid_num)
{
	unsigned int hash_log;
	struct bti_log_info info = {name, pid_num, pid_num};

	hash_log = cfi_hash_log(name, pid_num);

	if (find_cfi_warn(bti_hash_table, bti_counter, hash_log))
		return;

	update_cfi_hash_table(bti_hash_table, &bti_counter, hash_log);
	rtos_bti_write_msg(&info);
}
EXPORT_SYMBOL(rtos_bti_logger);
NOKPROBE_SYMBOL(rtos_bti_logger);
#endif
